// @ts-nocheck
const messageLength = 24

export class ByteBuf {
  _buffer: ArrayBuffer
  _raw: Uint8Array
  _view: DataView
  _littleEndian: boolean
  _implicitGrowth: boolean
  _index: number
  // Creates a new ByteBuf
  // - from given source (assumed to be number of bytes when numeric)
  // - with given byte littleEndian (defaults to little-endian)
  // - with given implicit growth strategy (defaults to false)
  constructor(source: number | ArrayBuffer = 0, littleEndian = false, implicitGrowth = true) {
    // Holds byte littleEndian
    this._littleEndian = littleEndian

    // Holds implicit growth strategy
    this._implicitGrowth = implicitGrowth

    // Holds read/write index
    this._index = 0

    // Attempt to extract a buffer from given source
    let buffer = this._extractBuffer(source, true)

    // On failure, assume source is a primitive indicating the number of bytes
    if (!buffer) {
      buffer = new ArrayBuffer(source as number)
    }

    // Assign new buffer
    this.buffer = buffer
  }

  // Sanitizes read/write index
  _sanitizeIndex() {
    if (this._index < 0) {
      this._index = 0
    }
    if (this._index > this.length) {
      this._index = this.length
    }
  }

  // Extracts buffer from given source and optionally clones it
  _extractBuffer(source, clone = false) {
    // Whether source is a byte-aware object
    if (source && typeof source.byteLength !== 'undefined') {
      // Determine whether source is a view or a raw buffer
      if (typeof source.buffer !== 'undefined') {
        return clone ? source.buffer.slice(0) : source.buffer
      }
      return clone ? source.slice(0) : source
      // Whether source is a sequence of bytes
    } else if (source && typeof source.length !== 'undefined') {
      // Although Uint8Array's constructor succeeds when given strings,
      // it does not correctly instantiate the buffer
      if (source.constructor === String) {
        return null
      }

      try {
        return (new Uint8Array(source)).buffer
      } catch (error) {
        return null
      }

      // No buffer found
    } else {
      return null
    }
  }

  // Retrieves buffer
  get buffer() {
    return this._buffer
  }

  // Sets new buffer and sanitizes read/write index
  set buffer(buffer) {
    this._buffer = buffer
    this._raw = new Uint8Array(this._buffer)
    this._view = new DataView(this._buffer)
    this._sanitizeIndex()
  }

  // Retrieves raw buffer
  get raw() {
    return this._raw
  }

  // Retrieves view
  get view() {
    return this._view
  }

  // Retrieves number of bytes
  get length() {
    return this._buffer.byteLength
  }

  // Retrieves number of bytes
  // Note: This allows for ByteBuf to be detected as a proper source by its own constructor
  get byteLength() {
    return this.length
  }

  // Retrieves byte littleEndian
  get littleEndian() {
    return this._littleEndian
  }

  // Sets byte littleEndian
  set littleEndian(littleEndian: boolean) {
    this._littleEndian = littleEndian
  }

  // Retrieves implicit growth strategy
  get implicitGrowth() {
    return this._implicitGrowth
  }

  // Sets implicit growth strategy
  set implicitGrowth(implicitGrowth: boolean) {
    this._implicitGrowth = implicitGrowth
  }

  // Retrieves read/write index
  get index() {
    return this._index
  }

  // Sets read/write index
  set index(index) {
    if (index < 0 || index > this.length) {
      throw new RangeError('Invalid index ' + index + ', should be between 0 and ' + this.length)
    }

    this._index = index
  }

  // Sets index to front of the buffer
  front() {
    this._index = 0
    return this
  }

  // Sets index to end of the buffer
  end() {
    this._index = this.length
    return this
  }

  // Seeks given number of bytes
  // Note: Backwards seeking is supported
  seek(bytes = 1) {
    this.index += bytes
    return this
  }

  // Retrieves number of available bytes
  get available() {
    return this.length - this._index
  }

  reader(method: string, bytes: number) {
    return () => {
      if (bytes > this.available) {
        throw new Error('Cannot read ' + bytes + ' byte(s), ' + this.available + ' available')
      }

      const value = this._view[method](this._index, this._littleEndian)
      this._index += bytes
      return value
    }
  }

  writer(method: string, bytes: number) {
    return (value) => {
      if (value == null) value = 0
      const available = this.available
      if (bytes > available) {
        if (this._implicitGrowth) {
          this.append(bytes - available)
        } else {
          throw new Error('Cannot write ' + value + ' using ' + bytes + ' byte(s), ' + available + ' available')
        }
      }

      this._view[method](this._index, value, this._littleEndian)
      this._index += bytes
      return this
    }
  }

  // Readers for bytes, shorts, integers, floats and doubles
  readByte = this.reader('getInt8', 1)
  readUnsignedByte = this.reader('getUint8', 1)
  readShort = this.reader('getInt16', 2)
  readUnsignedShort = this.reader('getUint16', 2)
  readInt = this.reader('getInt32', 4)
  readUnsignedInt = this.reader('getUint32', 4)
  readFloat = this.reader('getFloat32', 4)
  readDouble = this.reader('getFloat64', 8)

  // Writers for bytes, shorts, integers, floats and doubles
  writeByte = this.writer('setInt8', 1)
  writeUnsignedByte = this.writer('setUint8', 1)
  writeShort = this.writer('setInt16', 2)
  writeUnsignedShort = this.writer('setUint16', 2)
  writeInt = this.writer('setInt32', 4)
  writeUnsignedInt = this.writer('setUint32', 4)
  writeFloat = this.writer('setFloat32', 4)
  writeDouble = this.writer('setFloat64', 8)


  // Reads sequence of given number of bytes (defaults to number of bytes available)
  read(bytes = this.available) {
    if (bytes > this.available) {
      throw new Error('Cannot read ' + bytes + ' byte(s), ' + this.available + ' available')
    }

    if (bytes <= 0) {
      throw new RangeError('Invalid number of bytes ' + bytes)
    }

    const value = new ByteBuf(this._buffer.slice(this._index, this._index + bytes), this._littleEndian, this._implicitGrowth)
    this._index += bytes
    return value
  }

  // Writes sequence of bytes
  write(sequence) {
    let view

    // Ensure we're dealing with a Uint8Array view
    if (!(sequence instanceof Uint8Array)) {
      // Extract the buffer from the sequence
      const buffer = this._extractBuffer(sequence)
      if (!buffer) {
        throw new TypeError('Cannot write ' + sequence + ', not a sequence')
      }
      // And create a new Uint8Array view for it
      view = new Uint8Array(buffer)
    } else {
      view = sequence
    }

    const available = this.available
    if (view.byteLength > available) {
      if (this._implicitGrowth) {
        this.append(view.byteLength - available)
      } else {
        throw new Error('Cannot write ' + sequence + ' using ' + view.byteLength + ' byte(s), ' + this.available + ' available')
      }
    }

    this._raw.set(view, this._index)
    this._index += view.byteLength
    return this
  }

  // Reads UTF-8 encoded string of given number of bytes (defaults to number of bytes available)
  // Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L195)
  readString() {
    const bytes = this.readInt()
    if (bytes <= 0) return null

    if (bytes > this.available) {
      throw new Error('Cannot read ' + bytes + ' byte(s), ' + this.available + ' available')
    }

    // Local reference
    const raw = this._raw

    // Holds decoded characters
    const codepoints = []

    // Index into codepoints
    let c = 0

    // Bytes
    let b1 = null
    let b2 = null
    let b3 = null
    let b4 = null

    // Target index
    const target = this._index + bytes

    while (this._index < target) {
      b1 = raw[this._index]

      if (b1 < 128) {
        // One byte sequence
        codepoints[c++] = b1
        this._index++
      } else if (b1 < 194) {
        throw new Error('Unexpected continuation byte')
      } else if (b1 < 224) {
        // Two byte sequence
        b2 = raw[this._index + 1]
        if (b2 < 128 || b2 > 191) {
          throw new Error('Bad continuation byte')
        }
        codepoints[c++] = ((b1 & 0x1F) << 6) + (b2 & 0x3F)
        this._index += 2
      } else if (b1 < 240) {
        // Three byte sequence
        b2 = raw[this._index + 1]
        if (b2 < 128 || b2 > 191) {
          throw new Error('Bad continuation byte')
        }

        b3 = raw[this._index + 2]
        if (b3 < 128 || b3 > 191) {
          throw new Error('Bad continuation byte')
        }

        codepoints[c++] = ((b1 & 0x0F) << 12) + ((b2 & 0x3F) << 6) + (b3 & 0x3F)
        this._index += 3
      } else if (b1 < 245) {
        // Four byte sequence
        b2 = raw[this._index + 1]
        if (b2 < 128 || b2 > 191) {
          throw new Error('Bad continuation byte')
        }

        b3 = raw[this._index + 2]
        if (b3 < 128 || b3 > 191) {
          throw new Error('Bad continuation byte')
        }

        b4 = raw[this._index + 3]
        if (b4 < 128 || b4 > 191) {
          throw new Error('Bad continuation byte')
        }

        let cp = ((b1 & 0x07) << 18) + ((b2 & 0x3F) << 12) + ((b3 & 0x3F) << 6) + (b4 & 0x3F)
        cp -= 0x10000

        // Turn code point into two surrogate pairs
        codepoints[c++] = 0xD800 + ((cp & 0x0FFC00) >>> 10)
        codepoints[c++] = 0xDC00 + (cp & 0x0003FF)
        this._index += 4
      } else {
        throw new Error('Illegal byte')
      }
    }

    // Browsers may have hardcoded or implicit limits on the array length when applying a function
    // See: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply//apply_and_built-in_functions
    const limit = 1 << 16
    const length = codepoints.length
    if (length < limit) {
      return String.fromCharCode.apply(String, codepoints)
    }
    const chars = []
    for (let i = 0; i < length; i += limit) {
      chars.push(String.fromCharCode.apply(String, codepoints.slice(i, i + limit)))
    }
    return chars.join('')
  }

  // Writes UTF-8 encoded string
  // Note: Does not write string length or terminator
  // Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L264)
  writeString(string) {
    if (string == null || string.length === 0) {
      this.writeInt(0)
      return
    }

    // Encoded UTF-8 bytes
    const bytes = []

    // String length, offset and byte offset
    const length = string.length
    let i = 0
    let b = 0

    while (i < length) {
      const c = string.charCodeAt(i)

      if (c <= 0x7F) {
        // One byte sequence
        bytes[b++] = c
      } else if (c <= 0x7FF) {
        // Two byte sequence
        bytes[b++] = 0xC0 | ((c & 0x7C0) >>> 6)
        bytes[b++] = 0x80 | (c & 0x3F)
      } else if (c <= 0xD7FF || (c >= 0xE000 && c <= 0xFFFF)) {
        // Three byte sequence
        // Source character is not a UTF-16 surrogate
        bytes[b++] = 0xE0 | ((c & 0xF000) >>> 12)
        bytes[b++] = 0x80 | ((c & 0x0FC0) >>> 6)
        bytes[b++] = 0x80 | (c & 0x3F)
      } else {
        // Four byte sequence
        if (i === length - 1) {
          throw new Error('Unpaired surrogate ' + string[i] + ' (index ' + i + ')')
        }

        // Retrieve surrogate
        const d = string.charCodeAt(++i)
        if (c < 0xD800 || c > 0xDBFF || d < 0xDC00 || d > 0xDFFF) {
          throw new Error('Unpaired surrogate ' + string[i] + ' (index ' + i + ')')
        }

        const cp = ((c & 0x03FF) << 10) + (d & 0x03FF) + 0x10000

        bytes[b++] = 0xF0 | ((cp & 0x1C0000) >>> 18)
        bytes[b++] = 0x80 | ((cp & 0x03F000) >>> 12)
        bytes[b++] = 0x80 | ((cp & 0x000FC0) >>> 6)
        bytes[b++] = 0x80 | (cp & 0x3F)
      }
      ++i
    }

    this.writeInt(bytes.length)
    this.write(bytes)
    return bytes.length
  }

  writeLong(value: number | BigInt) {
    const available = this.available
    if (available < 8) {
      if (this._implicitGrowth) {
        this.append(8 - available)
      } else {
        throw new Error('Cannot write ' + value + ' using 8 byte(s), ' + available + ' available')
      }
    }

    if ('_setBigInt64' in DataView.prototype) {
      if (!(value instanceof BigInt)) {
        value = BigInt(value)
      }
      this._view.setBigInt64(this._index, value, this.littleEndian)
    } else {
      const isNegative = value < 0
      if (isNegative) value = -value
      let carrying = true
      for (let i = 0; i < 8; i++) {
        let byte = value & 0xFF
        if (isNegative) {
          if (carrying) {
            if (byte !== 0X00) {
              byte = (~byte + 1) & 0xFF
              carrying = false
            }
          } else {
            byte = (~byte) & 0xFF
          }
        }
        this._view.setUint8(this._index + (this.littleEndian ? i : 7 - i), byte)
        value = ~~(value / 256)
      }
    }
    this._index += 8
  }

  readLong() {
    let value = 0
    if ('_getBigInt64' in DataView.prototype) {
      value = Number(this._view.getBigInt64(this._index, this.littleEndian))
    } else {
      const isNegative = (this._view.getUint8(this._index + (this.littleEndian ? 7 : 0)) & 0x80) > 0
      let carrying = true
      for (let i = 0; i < 8; i++) {
        let byte = this._view.getUint8(this._index + (this.littleEndian ? i : 7 - i))
        if (isNegative) {
          if (carrying) {
            if (byte !== 0x00) {
              byte = (~(byte - 1)) & 0xFF
              carrying = false
            }
          } else {
            byte = (~byte) & 0xFF
          }
        }
        value += byte * 256 ** i
      }
      if (isNegative) {
        value = -value
      }
    }
    this._index += 8
    return value
  }

  readMessageId() {
    if (messageLength > this.available) {
      throw new Error('Cannot read ' + messageLength + ' byte(s), ' + this.available + ' available')
    }
    const raw = this._raw
    const chars = []
    let c = 0
    const target = this._index + messageLength
    while (this._index < target) {
      chars[c++] = String.fromCharCode(raw[this._index])
      this._index++
    }
    return chars.join('')
  }

  writeMessageId(messageId) {
    if (messageId == null || messageId.length !== messageLength) {
      throw new Error('Cannot write ' + messageId + ' as messageId')
    }

    const bytes = []
    const length = messageId.length
    for (let i = 0; i < length; i++) {
      bytes[i] = messageId.charCodeAt(i)
    }
    this.write(bytes)
    return bytes.length
  }

  // Prepends given number of bytes
  prepend(bytes) {
    if (bytes <= 0) {
      throw new RangeError('Invalid number of bytes ' + bytes)
    }

    const view = new Uint8Array(this.length + bytes)
    view.set(this._raw, bytes)
    this._index += bytes
    this.buffer = view.buffer
    return this
  }

  // Appends given number of bytes
  append(bytes) {
    if (bytes <= 0) {
      throw new RangeError('Invalid number of bytes ' + bytes)
    }

    const view = new Uint8Array(this.length + bytes)
    view.set(this._raw, 0)
    this.buffer = view.buffer
    return this
  }

  // Clips this buffer
  clip(begin = this._index, end = this.length) {
    if (begin < 0) {
      begin = this.length + begin
    }
    const buffer = this._buffer.slice(begin, end)
    this._index -= begin
    this.buffer = buffer
    return this
  }

  // Slices this buffer
  slice(begin = 0, end = this.length) {
    const slice = new ByteBuf(this._buffer.slice(begin, end), this.littleEndian)
    return slice
  }

  // Clones this buffer
  clone() {
    const clone = new ByteBuf(this._buffer.slice(0), this.littleEndian, this.implicitGrowth)
    clone.index = this._index
    return clone
  }

  // Reverses this buffer
  reverse() {
    Array.prototype.reverse.call(this._raw)
    this._index = 0
    return this
  }

  // Array of bytes in this buffer
  toArray() {
    return Array.prototype.slice.call(this._raw, 0)
  }

  // Short string representation of this buffer
  toString() {
    const littleEndian = (this._littleEndian) ? 'little-endian' : 'big-endian'
    return '[ByteBuf; littleEndian: ' + littleEndian + '; Length: ' + this.length + '; Index: ' + this._index + '; Available: ' + this.available + ']'
  }

  // Hex representation of this buffer with given spacer
  toHex(spacer = ' ') {
    return Array.prototype.map.call(this._raw, function (byte) {
      return ('00' + byte.toString(16).toUpperCase()).slice(-2)
    }).join(spacer)
  }

  // ASCII representation of this buffer with given spacer and optional byte alignment
  toASCII(spacer = ' ', align = true, unknown = '\uFFFD') {
    const prefix = (align) ? ' ' : ''
    return Array.prototype.map.call(this._raw, function (byte) {
      return (byte < 0x20 || byte > 0x7E) ? prefix + unknown : prefix + String.fromCharCode(byte)
    }).join(spacer)
  }
}


